概要
この記事では、Unreal Insight を使用して最適化の負荷調査を行う流れと、ゲーム内でアセットをLoadSynchronous()
で同期的に読み込む際に発生するヒッチを防ぐための対策として、C++による非同期ロード(裏読み)の実装方法を紹介します。Blueprint での非同期ロードについては、下記の参考リンクを参照してください。
環境
- Rider 2024.2.6
- Unreal Engine 5.4
- Windows 11 Pro
参考資料
- 公式ドキュメント Asynchronous Asset Loading
- [UE4] Asset Manager のアセットの非同期ロード機能について その 1 ( 非同期ロードの解説 & レベルの裏読み編 )
- Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022 特に 22:40〜28:15 の部分で非同期ロードに関する情報が、27:33 には Blueprint による非同期ロード(AsyncLoad)の実装方法が紹介されています。
- [UE5] UnrealInsights を使ってみよう
- Unreal Insights
本編
Unreal Insight を使った最適化の負荷調査
Unreal Insights って? Unreal Insights は、Unreal Engine でのパフォーマンスやメモリの使用状況を分析するためのツールです。公式ドキュメントはこちら: : https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-in-unreal-engine
使用方法
詳しい使用方法については [UE5] UnrealInsights を使ってみよう を参照してください。
最適化調査を行う際には、通常の PIE(Play In Editor)モードではなく、ターゲットプラットフォームのパッケージ版を使用するのが理想です。PIE モードでは事前に読み込まれたキャッシュや背景での動作が影響し、正確な負荷測定が難しくなるためです。
今回調査のは PIE よりパッケージ版の環境に近い Standalone Game で調査を行います。
Standalone Game の起動方法
下図のように、UE エディターから Standalone Game を起動します。
ゲームプレイ中にヒッチ(カクつき)が発生した場合、次のコマンドを活用しましょう。
stat unitgraph
「stat UnitGraph」コマンドは、ゲームの処理負荷をグラフ形式で可視化するツールです。これにより、ヒッチが発生するタイミングでグラフに大きなスパイクが表示され、問題の箇所を明確に特定できます。
多くの人がよく使用する「stat Unit」や「stat fps」とは異なり、「stat UnitGraph」では短時間のヒッチもしっかりグラフに残るため、見逃すことが少ないです。
実行後は、以下のようなグラフが表示されます。左下には負荷のグラフ、右上には具体的な数値が表示されます。60fps を目指す場合は、Frame 時間を16.6ms以下に保つことが理想です。
次に、Unreal Insight を使って実際に計測を行います。
trace.start
と trace.stop
Unreal Insight で計測を始めるには、trace.start
コマンドを実行します。計測が完了したら、trace.stop
で終了します。
処理負荷計測結果
下図の動画のようにヒッチの発生が確認しました。
重い処理が実行されると、グラフ上に大きな変化が現れます。
Game: 58.26ms ゲームスレッドに 58.26ms もの時間がかかっているのがわかります。
次に、Unreal Insight の計測結果も確認してみましょう。
Trace を開く方法
Trace データを開くと、以下の結果が表示されます。
この結果で注目すべきは、緑のバーで示されている
LoadObject (154.7ms) - /Game/Main/InGame/VFX/Niagara/NS_DizzyStar.NS_DizzyStar
です。ここから、Niagara エフェクトのローディング処理が大きな負荷をかけていることがわかります。
特に、Niagara の初回ロード時や初回スポーン時に、シェーダーのコンパイルが原因でヒッチが発生します。
今回は初回ロード時にヒッチが発生する原因について説明しますが、もし初回スポーン時にヒッチが発生する場合は、事前に(暗転などの演出中に)一度見えない場所でスポーンさせることで解決することが可能です。
パッケージ版の場合、インストール後の最初の 1 回だけこのヒッチが発生します。 UE では UE 起動後初回のみ発生します。
ヒッチを再現したい場合は、Unreal Engine を再起動するか、パッケージを削除して再インストールする必要があります。
これが原因の Niagara エフェクトです。
C++側のコードを見ると、ローディング処理は次のようになっています。
PlayerCharacter.h1public: 2 //... 3 UPROPERTY(EditAnywhere, BlueprintReadWrite) 4 TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset; 5 //...
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.LoadSynchronous(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
エフェクトをスポーンする直前にLoadSynchronous()
でエフェクトのアセットを同期にロードしていることが、ヒッチの原因となっています。
LoadSynchronous()
(同期ロード)はロード完了するまで待つ(他の処理を止める)ということです。これによりプレイヤーはヒッチが感じます。
UE 公式では非同期ロードを推奨します。
参考動画: Maximizing Your Game's Performance in Unreal Engine | Unreal Fest 2022
動画の 22:40 から 28:15 までは非同期ロード関連になります
27:33 には Blueprint での非同期ロード(AsyncLoad)の実装方法です
TSoftObjectPtr<UNiagaraSystem> DizzyEffectAsset
エフェクトのアセットはプレイヤーに持たれています。
調査結果:原因はエフェクトスポーンする直前のLoadSynchoronous()
です。
非同期ロード(AsyncLoad)を実装する
同期ロードによるヒッチを解消するために、ゲーム開始時にアセットを事前に非同期ロード(裏読み)します。
非同期ロードには時間がかかる場合があるため、事前に非同期ロードの時間を確保しないと、アセットのスポーンが間に合わないことがあります。
これから、C++で非同期ロードの関数を作成します。
PlayerCharacter.h1protected: 2 void OnDizzyEffectLoaded(); 3 void LoadDizzyEffectAsset();
PlayerCharacter.cpp1void APlayerCharacter::BeginPlay() 2{ 3 Super::BeginPlay(); 4 LoadDizzyEffectAsset(); 5} 6 7void APlayerCharacter::LoadDizzyEffectAsset() 8{ 9 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset requeset load")); 10 UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(DizzyEffectAsset.ToSoftObjectPath(), 11 FStreamableDelegate::CreateUObject( 12 this, &APlayerCharacter::OnDizzyEffectLoaded)); 13} 14 15void APlayerCharacter::OnDizzyEffectLoaded() 16{ 17 UE_LOG(LogTemp, Log, TEXT("DizzyEffectLoaded")); 18 19 if (IsValid(DizzyEffectAsset.Get())) 20 { 21 UE_LOG(LogTemp, Log, TEXT("DizzyEffectAsset is valid")); 22 } 23 else 24 { 25 UE_LOG(LogTemp, Error, TEXT("DizzyEffectAsset is null, Function name: %s"), *FString(__FUNCTION__)); 26 } 27}
次に、アセットの同期ロードをGet()
に置換
PlayerCharacter.cpp1void APlayerCharacter::StartDizzy() 2{ 3 if (IsDizzy) 4 { 5 return; 6 } 7 CharacterMovementComponent->MaxWalkSpeed = DizzySpeed; 8 9 10 IsDizzy = true; 11 12 UNiagaraSystem* DizzyEffectSystem = DizzyEffectAsset.Get(); 13 if (!IsValid(DizzyEffectSystem)) 14 { 15 UE_LOG(LogTemp, Error, TEXT("DizzyEffectSystem is null, Function name: %s"), *FString(__FUNCTION__)); 16 } 17 DizzyEffect = UNiagaraFunctionLibrary::SpawnSystemAttached(DizzyEffectSystem, SceneComponent, NAME_None, 18 DizzyEffectOffset, FRotator::ZeroRotator, 19 EAttachLocation::KeepRelativeOffset, true); 20 21 if (!IsValid(DizzySoundAsset)) 22 { 23 UE_LOG(LogTemp, Error, TEXT("DizzySound is null, Function name: %s"), *FString(__FUNCTION__)); 24 } 25 else 26 { 27 DizzySound = UGameplayStatics::SpawnSoundAtLocation(GetWorld(), DizzySoundAsset, GetActorLocation()); 28 } 29 GetWorldTimerManager().SetTimer(DizzyTimerHandle, this, &APlayerCharacter::EndDizzy, DizzyDuration, false); 30}
これで非同期ロードの実装が完了しました。 次に、Standalone ゲーム内に確認してみましょう。
ログでDizzyEffectAsset is valid
と表示されば、アセットの非同期ロード(AsyncLoad)が成功したことが確認できます。
結果
「stat UnitGraph」コマンドを使用した結果、グラフ上のスパイクがなくなり、ヒッチが解消されました。
まとめ
調査の流れ
- ヒッチの検出:ゲーム中に発生したヒッチを「stat unitgraph」コマンドを使ってグラフ化し、重い処理が発生する箇所を特定。
- Unreal Insight での計測:ヒッチ発生時の詳細な負荷を調査するため、trace.start および trace.stop コマンドを使用してトレースデータを収集し、負荷の発生源を確認。
問題の特定
- エフェクトのアセット(Niagara エフェクト)が同期ロード
LoadSynchronous()
によって読み込まれていたため、ロードが完了するまで他の処理が止まり、ヒッチが発生していることが判明。
非同期ロードの実装
- ヒッチを回避するために、TSoftObjectPtr を使用して、エフェクトをゲーム開始時に非同期でロードする処理を追加。これにより、プレイヤーがエフェクトを使用する前にアセットが読み込まれ、ヒッチを防ぎます。
結果
非同期ロードの実装により、ヒッチが解消され、ゲームプレイがスムーズになったことが確認されました。 このように、非同期ロードを用いることでゲームパフォーマンスを向上させ、プレイヤーの体験を改善できました。